热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

可能会|表里_深入理解DiskLruCache源码

篇首语:本文由编程笔记#小编为大家整理,主要介绍了深入理解DiskLruCache源码相关的知识,希望对你有一定的参考价值。作

篇首语:本文由编程笔记#小编为大家整理,主要介绍了深入理解DiskLruCache源码相关的知识,希望对你有一定的参考价值。




作者:岩浆李的游鱼leo2



前言

我们在用第三方框架的时候,比如glide,okhttp等,使用起来已经轻而易举,因为我们只管用。忽视了其用到的缓存技术。让我们一起来理解下缓存的本质。内存缓存一般现在用的是LruCache缓存,磁盘缓存是DiskLruCache。它们用的都是LRU算法(最近最少使用)。网络缓存当然就是网络请求,数据放在后端数据库了。

LRU算法:Least Recently Used 即为近期最少使用。在缓存数据的时候,如果数据不存在缓存中,则放入缓存中,如存在缓存中,会将缓冲重新放入头部位置表示最近使用了,底部位置则为近期最少使用,在一些配置下,如果达到了缓存的容量下,那么要缓存新的数据,则需要从底部最近最少使用的开始删除缓存。

看到这里,其实LRU也有他的缺点。比如遇到这样的场景,一般电商都会有秒杀日,那么一旦出现秒杀日,那么在秒杀日当天可能会因为缓存容量的问题,把真正潜在经常访问的缓存给删除。怎么优化的,其实也有办法,LRU+2Q。有点偏题了,感兴趣的同学可以自行了解。

接下来跟着我节奏,我们将DiskLruCache各个击破。当然你需要先下载一份DiskLruCache源码。
点这里-源码地址;摩拜JakeWharton大神


一、DiskLruCache的创建

DiskLruCache mDiskLruCache = null;
try
File cacheDir = getDiskCacheDir(this, "bitmap");
if (!cacheDir.exists())
cacheDir.mkdirs();

mDiskLruCache = DiskLruCache.open(cacheDir, getVersionCode(this), 1, 10 * 1024 * 1024);
catch (IOException e)
e.printStackTrace();

做完以上呢我们打开手机的缓存文件,因为android手机太杂,老一点带sd卡的你可以从这个路径里去找/sdcard/Android/data/项目包名/cache/bitmap/。内置储存卡的,可以通过点击手机内部存储–> Android/data/项目包名/cache/bitmap/ 在这个文件夹可以看到一个 journal文件。先介绍完open后,会介绍journal文件的

可以看到DiskLruCahe是通过open创建的,其中包括4个参数,我们来看下源码。源码就直接给了注解,我会直接翻译成中文

/**
* 参数1:directory 缓存路径位置,如果缓存文件不存在就新建
* 参数2:appVersion 当前app的版本号,也就是versionCode
* 参数3:valueCount 一个缓存key的缓存个数。一般传1,表示1个key对应1个缓存
* 参数4:maxSize 缓存的最大容量
*/

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException
if (maxSize <&#61; 0)
throw new IllegalArgumentException("maxSize <&#61; 0");

if (valueCount <&#61; 0)
throw new IllegalArgumentException("valueCount <&#61; 0");

//new出当前文件&#xff0c;也就是对成员变量赋值&#xff0c;如directory&#xff0c;appVersion&#xff0c;valueCount&#xff0c;journalFile等
DiskLruCache cache &#61; new DiskLruCache(directory, appVersion, valueCount, maxSize);
//那为什么我们调用的方法是open呢? 看下面的判断的这个判断&#xff0c;大致意思是&#xff1a;
//如果我们传入的参数&#xff0c;和原始缓存文件一致&#xff0c;那么返回原始缓存文件&#xff0c;并在上面拼接
//如果缓存文件journal存在的话
if (cache.journalFile.exists())
try
//就是去读原始缓存文件里的参数&#xff0c;和现在赋值的参数是否一致&#xff0c;不一致的话抛出异常
//同时将日志信息读取到我们的缓存链表里LinkedHashMap lruEntries 这里会放到【1.1、readJournal()】详细讲解
cache.readJournal();
//这里会将上次读取还处于头部文件为 DIRTY 即还未写入缓存的脏数据清掉。会放到【1.2、processJournal()】详细讲解
cache.processJournal();
//看这个append为true&#xff0c;内部调用的是new FileOutputStream(file, append)&#xff0c;输出流在原始文件上拼接
//如果这个参数为false的话&#xff0c;会把原始文件内容清掉&#xff0c;重新写。断点下载就是这个原理。有兴趣的同学可自了解
cache.journalWriter &#61; new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
catch (IOException journalIsCorrupt)
//抛出异常的话&#xff0c;那么把原始缓存文件删除&#xff0c;在下面新建缓存文件
cache.delete();


//如果走到这一步&#xff0c;说明上面没有把原始缓存return出去。且还走了catch里把原始缓存文件删除了&#xff0c;那么按新的参数&#xff0c;生成缓存文件
directory.mkdirs();
cache &#61; new DiskLruCache(directory, appVersion, valueCount, maxSize);
//重建缓存文件&#xff0c;会放到【1.3、rebuildJournal();】详细讲解
cache.rebuildJournal();
return cache;

创建缓存代码也贴上来&#xff0c;方便读者阅览。

// 获取缓存路径&#xff0c;这里已经做好了判断是否有外部存储
public File getDiskCacheDir(Context context, String uniqueName)
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable())
cachePath &#61; context.getExternalCacheDir().getPath();
else
cachePath &#61; context.getCacheDir().getPath();

return new File(cachePath &#43; File.separator &#43; uniqueName);

//获取app当前的版本号versionCode
public int getVersionCode(Context context)
PackageManager packageManager &#61; context.getPackageManager();
PackageInfo packageInfo;
int versionCode &#61; 1;
try
packageInfo &#61; packageManager.getPackageInfo(context.getPackageName(), 0);
versionCode &#61; packageInfo.versionCode;
catch (PackageManager.NameNotFoundException e)
e.printStackTrace();

return versionCode;


1.1、readJournal()

private void readJournal() throws IOException
//把journal文件通过输入流读入
InputStream in &#61; new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
try
//这几句就是读出原始缓存文件journal的参数和现在通过open传入的参数进行对比&#xff0c;如果参数不一致那么会抛出异常
String magic &#61; readAsciiLine(in);
String version &#61; readAsciiLine(in);
String appVersionString &#61; readAsciiLine(in);
String valueCountString &#61; readAsciiLine(in);
String blank &#61; readAsciiLine(in);
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank))
throw new IOException("unexpected journal header: ["
&#43; magic &#43; ", " &#43; version &#43; ", " &#43; valueCountString &#43; ", " &#43; blank &#43; "]");

while (true)
try
//把缓存的日志信息&#xff0c;读入到我们的缓存链表LinkedHashMap lruEntries
//这里还有一个操作&#xff0c;把头部REMOVE的日志清掉&#xff0c;属于冗余日志
readJournalLine(readAsciiLine(in));
catch (EOFException endOfJournal)
break;


finally
closeQuietly(in);


再来看下readJournalLine()的源码

private void readJournalLine(String line) throws IOException
String[] parts &#61; line.split(" ");
if (parts.length < 2)
throw new IOException("unexpected journal line: " &#43; line);

String key &#61; parts[1];
//这里就是日志信息里的已经REMOVE的缓存&#xff0c;从缓存列表里移除
if (parts[0].equals(REMOVE) && parts.length &#61;&#61; 2)
lruEntries.remove(key);
return;

Entry entry &#61; lruEntries.get(key);
if (entry &#61;&#61; null)
entry &#61; new Entry(key);
lruEntries.put(key, entry);

//读出头部为CLEAN&#xff0c;表示已经缓存成功的数据的一些信息。并复赋值到缓存列表里
//缓存列表里存的Entry对象&#xff0c;其句是DiskLruCache定义的内部类&#xff0c;用于存储缓存信息用的。
if (parts[0].equals(CLEAN) && parts.length &#61;&#61; 2 &#43; valueCount)
entry.readable &#61; true;
entry.currentEditor &#61; null;
entry.setLengths(copyOfRange(parts, 2, parts.length));
else if (parts[0].equals(DIRTY) && parts.length &#61;&#61; 2)
entry.currentEditor &#61; new Editor(entry);
else if (parts[0].equals(READ) && parts.length &#61;&#61; 2)
// this work was already done by calling lruEntries.get()
else
throw new IOException("unexpected journal line: " &#43; line);


这个时候你可能会想了&#xff0c;即使不断的操作缓存&#xff0c;是不是journal日志文件会越来越大呢&#xff1f;引用一下郭神的原话



如果我不停频繁操作的话&#xff0c;就会不断地向journal文件中写入数据&#xff0c;那这样journal文件岂不是会越来越大&#xff1f;这倒不必担心&#xff0c;DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数&#xff0c;每执行一次写入、读取或移除缓存的操作&#xff0c;这个变量值都会加1&#xff0c;当变量值达到2000的时候就会触发重构journal的事件&#xff0c;这时会自动把journal中一些多余的、不必要的记录全部清除掉&#xff0c;保证journal文件的大小始终保持在一个合理的范围内。


然后我仔细阅读了源码发现&#xff0c;其实在每次操作get()&#xff0c;completeEdit()&#xff0c;remove都调用了一句这样的代码

if (journalRebuildRequired())
executorService.submit(cleanupCallable);

那么看journalRebuildRequired()&#xff0c;字面意思很明显&#xff0c;jornal文件是否需要被重建

//一看源码和郭神所说一致
private boolean journalRebuildRequired()
final int REDUNDANT_OP_COMPACT_THRESHOLD &#61; 2000;
return redundantOpCount >&#61; REDUNDANT_OP_COMPACT_THRESHOLD
&& redundantOpCount >&#61; lruEntries.size();

不难想到cleanupCallable里发生了什么&#xff0c;这里除了缓存超过最大缓存&#xff0c;清理缓存外&#xff0c;也判断了journal文件是否需要被重建。将redundantOpCount置为0

if (journalRebuildRequired())
rebuildJournal();
redundantOpCount &#61; 0;


1.2、processJournal()

processJournal()是在执行完readJournal()后执行的。此时缓存文件信息已全部读入到了缓存列表里LinkedHashMap lruEntries。

processJournal()其实就是把上次还未缓存成功&#xff0c;停留在头部信息为DIRTY的缓存数据&#xff0c;即为脏数据给清除掉。&#xff08;数据缓存成功即有2行缓存日志&#xff0c;头部信息分别为DIRTY&#xff0c;CLEAN&#xff1b;移除成功&#xff0c;头部信息也有2行分别为DIRTY&#xff0c;REMOVE&#xff1b;会在讲解journal说清楚&#xff0c;现在跟着“感觉”走就行了&#xff09;上源码

private void processJournal() throws IOException
//这个时候还做了个操作把journalFileTmp也就是journal的临时文件删除了&#xff0c;
//因为这个时候没走rebuildJournal()重建journal文件的方法&#xff0c;所以journalFileTmp已经没有存在的必要了&#xff0c;删除文件
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i &#61; lruEntries.values().iterator(); i.hasNext(); )
Entry entry &#61; i.next();
//已经被缓存&#xff0c;那么缓存size&#43;&#43;
if (entry.currentEditor &#61;&#61; null)
for (int t &#61; 0; t < valueCount; t&#43;&#43;)
size &#43;&#61; entry.lengths[t];

else
//没有被缓存&#xff0c;那么清除掉这条脏数据
entry.currentEditor &#61; null;
for (int t &#61; 0; t < valueCount; t&#43;&#43;)
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));

i.remove();




1.3、rebuildJournal()

这是在open&#xff08;&#xff09;方法内&#xff0c;参数不一致&#xff0c;最终会走到journal文件重建的方法

private synchronized void rebuildJournal() throws IOException
if (journalWriter !&#61; null)
journalWriter.close();

//这块就是journal的头信息&#xff0c;你也可以直接把他认为是参数信息
Writer writer &#61; new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
writer.write(MAGIC);
writer.write("\\n");
writer.write(VERSION_1);
writer.write("\\n");
writer.write(Integer.toString(appVersion));
writer.write("\\n");
writer.write(Integer.toString(valueCount));
writer.write("\\n");
writer.write("\\n");
//把当前已经缓存成功的日志信息写上去
for (Entry entry : lruEntries.values())
if (entry.currentEditor !&#61; null)
writer.write(DIRTY &#43; &#39; &#39; &#43; entry.key &#43; &#39;\\n&#39;);
else
writer.write(CLEAN &#43; &#39; &#39; &#43; entry.key &#43; entry.getLengths() &#43; &#39;\\n&#39;);


writer.close();
//注意&#xff0c;这里先是把数据写进临时文件journalFileTmp里&#xff0c;最后改名为journalFile里
//其实这个时候我也在想&#xff0c;这个临时文件就是个多余的存在。纵观了代码&#xff0c;也就在这起了点作用。journalFile文件在前面
//被delete掉了&#xff0c;难道不能在这里从新new下&#xff0c;取代这个journalFileTmp吗&#xff1f;我觉得是可以的&#xff0c;可能大神想把初始化工作都
//放在open()里吧
journalFileTmp.renameTo(journalFile);
journalWriter &#61; new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);


二、journal文件

本来想把这个放在最后说&#xff0c;起着画龙点睛的左右。但是前面提了这么多&#xff0c;如果有看到这的朋友&#xff0c;其实再这点这龙的眼睛是最合适的。承上启下&#xff0c;贯穿全文。

通过上面的创建&#xff0c;我们打开手机&#xff0c;可以看到journal文件生成了。

因为我是新建的&#xff0c;里面还没存数据&#xff0c;所以日志信息是不全的。加上缓存信息&#xff0c;是第三步的内容。所以暂且用源码的地址讲解&#xff0c;大神也注释的非常清楚了。这里我们只要知道操作缓存的话&#xff0c;这些操作信息都是会被记录到journal文件里的。

/*
* libcore.io.DiskLruCache
* 1
* 1
* 1
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
*/


  • 第一行&#xff1a;libcore.io.DiskLruCache 标志着我们使用的是DiskLruCache技术
  • 第二行&#xff1a;1 DiskLruCache的版本号&#xff0c;这个值是恒为1的
  • 第三行: 1 app的版本号&#xff0c;也就是对应着我们从open里传入的值
  • 第四行&#xff1a;1 即valueCount&#xff0c;如果是1那么我们的key和缓存是一对一的关系
  • 第五行&#xff1a;空行&#xff0c;前五行被成为journal文件的头

DIRTY头部 : 目前这是一条脏数据&#xff0c;正准备缓存&#xff0c;但还未缓存成功。后面跟着的是key值&#xff0c;因为需要一一对应&#xff0c;且不允许带符号&#xff0c;这个key是通过图片url地址进行MD5加密得来

CLEAN头部 : 该key值对应的缓存数据&#xff0c;已成功缓存。key值后面对应的数字是缓存的大小&#xff0c;源码里跟了2个&#xff0c;说明他valueCount设置成了2&#xff0c;一个key对应多个缓存

REMOVE头部 : 该key值对应的缓存数据&#xff0c;被清除了

READ头部 : 该key值对应的缓存数据&#xff0c;被使用了

可以看到每一行DIRTY的key&#xff0c;后面都应该有一行对应的CLEAN或者REMOVE的记录&#xff0c;否则这条数据就是“脏”的&#xff0c;会被自动删除掉。我们在第1小结里&#xff0c;已经讲到过了。journal大概介绍就讲完了。如果你对下方知识点不管感兴趣&#xff0c;可以跳过

这个时候可能会有一个疑问&#xff0c;为什么key还能一对多呢&#xff1f;这里引入一个知识点&#xff0c;结合这个知识点&#xff0c;可能你就理解了。没错哈希碰撞

什么是 哈希碰撞 &#xff1f;
我们要存数据&#xff0c;首先要把存储的key值转换成系统认识的标记。举个列子&#xff0c;字母‘a’和数子97的ASCII码都是 97&#xff0c;转换成二进制是&#xff1a;01100001。所以这样就造成了哈希碰撞&#xff0c;即key计算出来的hashcode是一样的值。

那么解决了hash碰撞的问题&#xff0c;岂不是同一key可以对应多个值&#xff1f;有很多解决方式&#xff0c;这里我们说下拉链法&#xff1a;即如果出现哈希碰撞的key,对应的不是一个value&#xff0c;而是一个单项链表的地址。先看下其数据结构:

static class Entry<K,V> implements Map.Entry<K,V>
final K key;
V value;
Entry<K,V> next;
int hash;...

单项列表就是通过next,next&#xff0c;有碰撞就存下去。当我们通过key去取value的时候&#xff0c;这个key对应的是哈希碰撞的话&#xff0c;就会取到一个单列表&#xff0c;然后通过遍历&#xff08;因为数据结构里有具体的key值&#xff0c;key值对应上&#xff09;&#xff0c;得到最终的value。因为是遍历的&#xff0c;所以这里并不适合存太长&#xff0c;size满8的话会转换成红黑树&#xff0c;提高查询效率。


三、写入缓存


3.1、DiskLruCache.Editor

写入缓存是通过DiskLruCache.Editor 这类&#xff0c;后面跟的是写入缓存的key&#xff0c;这个key呢我们只要保证不带什么特殊符号且唯一性即可&#xff0c;这里用郭神的方案&#xff0c;我们使用MD5加密

try
String imageUrl &#61; "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54a4b40807034b3090708c935689345f~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?";
String key &#61; hashKeyForDisk(imageUrl);
DiskLruCache.Editor editor &#61; mDiskLruCache.edit(key);
catch (IOException e)
e.printStackTrace();

public String hashKeyForDisk(String key)
String cacheKey;
try
final MessageDigest mDigest &#61; MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey &#61; bytesToHexString(mDigest.digest());
catch (NoSuchAlgorithmException e)
cacheKey &#61; String.valueOf(key.hashCode());

return cacheKey;

private String bytesToHexString(byte[] bytes)
StringBuilder sb &#61; new StringBuilder();
for (int i &#61; 0; i < bytes.length; i&#43;&#43;)
String hex &#61;

推荐阅读
  • 深入解析Redis内存对象模型
    本文详细介绍了Redis内存对象模型的关键知识点,包括内存统计、内存分配、数据存储细节及优化策略。通过实际案例和专业分析,帮助读者全面理解Redis内存管理机制。 ... [详细]
  • 异常要理解Java异常处理是如何工作的,需要掌握一下三种异常类型:检查性异常:最具代表性的检查性异常是用户错误或问题引起的异常ÿ ... [详细]
  • 通过Web界面管理Linux日志的解决方案
    本指南介绍了一种利用rsyslog、MariaDB和LogAnalyzer搭建集中式日志管理平台的方法,使用户可以通过Web界面查看和分析Linux系统的日志记录。此方案不仅适用于服务器环境,还提供了详细的步骤来确保系统的稳定性和安全性。 ... [详细]
  • 不确定性|放入_华为机试题 HJ9提取不重复的整数
    不确定性|放入_华为机试题 HJ9提取不重复的整数 ... [详细]
  • 在Java中,this是一个引用当前对象的关键字。如何通过this获取并显示其所指向的对象的属性和方法?本文详细解释了this的用法及其背后的原理。 ... [详细]
  • 编程挑战:2019 Nitacm 校赛 D 题 - 雷顿女士与分队(高级版)
    本文深入解析了2019年Nitacm校赛D题——雷顿女士与分队(高级版),详细介绍了问题背景、解题思路及优化方案。 ... [详细]
  • Git管理工具SourceTree安装与使用指南
    本文详细介绍了Git管理工具SourceTree的安装、配置及团队协作方案,旨在帮助开发者更高效地进行版本控制和项目管理。 ... [详细]
  • 本题探讨了在一个有向图中,如何根据特定规则将城市划分为若干个区域,使得每个区域内的城市之间能够相互到达,并且划分的区域数量最少。题目提供了时间限制和内存限制,要求在给定的城市和道路信息下,计算出最少需要划分的区域数量。 ... [详细]
  • 本文详细介绍了 Java 中的 org.apache.hadoop.registry.client.impl.zk.ZKPathDumper 类,提供了丰富的代码示例和使用指南。通过这些示例,读者可以更好地理解如何在实际项目中利用 ZKPathDumper 类进行注册表树的转储操作。 ... [详细]
  • 对象自省自省在计算机编程领域里,是指在运行时判断一个对象的类型和能力。dir能够返回一个列表,列举了一个对象所拥有的属性和方法。my_list[ ... [详细]
  • Kubernetes 持久化存储与数据卷详解
    本文深入探讨 Kubernetes 中持久化存储的使用场景、PV/PVC/StorageClass 的基本操作及其实现原理,旨在帮助读者理解如何高效管理容器化应用的数据持久化需求。 ... [详细]
  • 本文介绍如何使用 Android 的 Canvas 和 View 组件创建一个简单的绘图板应用程序,支持触摸绘画和保存图片功能。 ... [详细]
  • 探索如何使用公共数据集为您的编程项目提供动力。无论您是编程新手还是有经验的开发者,本文将为您提供实用建议和资源,帮助您启动并运行一个创新的数据驱动型项目。 ... [详细]
  • 本文深入探讨了POJ2762问题,旨在通过强连通分量缩点和单向连通性的判断方法,解决有向图中任意两点之间的可达性问题。文章详细介绍了算法原理、实现步骤,并附带完整的代码示例。 ... [详细]
  • 解决SVN图标显示异常问题的综合指南
    本文详细探讨了SVN图标无法正常显示的问题,并提供了多种有效的解决方案,涵盖不同环境下的具体操作步骤。通过本文,您将了解如何排查和修复这些常见的SVN图标显示故障。 ... [详细]
author-avatar
mobiledu2502909113
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有